/*
 * ExampleSSLClient.c
 *
 * This program is an OpenSSL client that talks to a server over an SSL
 * connection.  Once a connection is made, the client sends a "message"
 * (defined as a stream terminated by <LF>) to the server, and reads
 * back the response which is a message sent back by the server.
 * The code to perform the SSL stuff is (as much as possible) all contained
 * in "main" - the use of other functions is limited to just displaying
 * information, so that you should only need to read the "main" code to
 * see what order things are done.
 *
 * Environment variables:
 *   truststore - assumed to point at .PEM file containing certificates
 *                that are trusted by this client
 *
 * Tested on
 * Tru64 UNIX V5.1, OpenSSL 0.9.6c
 * OpenVMS V7.2-1, OpenSSL 0.9.5a with "CC/POINTER=64"
 *
 * Nick Hudson, May 2002
 */
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/rand.h>
#include <openssl/err.h>

#include <ctype.h>

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <netdb.h>

int isTracing = 0;
char *randbuf = 
"this isn't a very good way to seed the PRNG but will suffice"
"this isn't a very good way to seed the PRNG but will suffice"
"this isn't a very good way to seed the PRNG but will suffice"
"this isn't a very good way to seed the PRNG but will suffice"
"this isn't a very good way to seed the PRNG but will suffice";


/* Print SSL errors and exit*/
void error_exit(char *string)
{
  printf("%s : \n",string);
  ERR_print_errors_fp(stdout);
  exit(0);
}

/*
 * If "doTrace" is non-zero, then send the message to the stdout
 */
void trace(int doTrace, const char *message) 
{
  if (doTrace) {
    printf("%s\n",message);
  }
}

void usage()
{
  trace(1,"\nUsage: ExampleSSLClient <server> <port> <ciphers>"
	" <protocol> [trace]");
  trace(1," server      : the server to talk to");
  trace(1," port        : port number to use");
  trace(1," ciphers     : types of cipher suites to propose :-");
  trace(1,"               'c' : suites that use server-cert");
  trace(1,"               'a' : anonymous (no server-cert)");
  trace(1,"               'n' : suites with no encryption");
  trace(1," protocol    : '1' '2' or '3' or '4' or '5' where :-");
  trace(1,"               '1' means TLSv1");
  trace(1,"               '2' means TLSv1.1");
  trace(1,"               '3' means TLSv1.2");
  trace(1,"               '4' means SSLv3");
  trace(1,"               '5' means SSLv23");
  trace(1," trace       : (optional) tracing to enable :-");
  trace(1,"               't' : turn on program tracing");
}

/*
 * This method looks at the run-time environment in an attempt to check
 * if things are set up correctly for the program to run.  It reports
 * areas of concern but does not abort
 */
void checkEnvironment()
{
  // Check whether they've specified a location for trusted certificates
  if (getenv("truststore") == NULL) {
    trace(1,
"\nThe environment variable 'truststore' is not defined, which means that");
    trace(1,
"this program has no way to verify certificates sent by a server during");
    trace(1,
"cipher negotiation.  Set the environment variable 'truststore' to point");
    trace(1,
	  "at a suitable file if you need to use one.\n");
    }
  
}


/*
 * Display all the ciphers available for a specific SSL structure
 */
void enumerate_ciphers(SSL *ssl)
{
  char tracebuf[512];
  
  int index = 0;
  const char *next = NULL;
  do {
    next = SSL_get_cipher_list(ssl,index);
    if (next != NULL) {
      sprintf(tracebuf,"  %s",next);
      trace(1,tracebuf);
      index++;
    }
  }
  while (next != NULL);
}

/*
 * Create a socket connected to specified host:port
 * Returns the id of a connected socket.  If any operation in
 * the function fails, then error_exit is called and the program
 * will terminate.
 */
int tcpConnect(char *host, int port)
{
    struct hostent *he;
    struct sockaddr_in SA;
    int stat;
    int sock;
    
    sock = socket(AF_INET,SOCK_STREAM,0);

    if (sock == -1) {
      error_exit("Call to create socket failed");
    }

    he = gethostbyname(host);
    if (he == NULL) {
      error_exit("Server name supplied was unrecognized");
    }

    memset(&SA,0,sizeof(SA));
    SA.sin_addr=*(struct in_addr*)he->h_addr_list[0];
    SA.sin_family=AF_INET;
    SA.sin_port=htons(port);
    
    stat = connect(sock,
		   (struct sockaddr *)&SA,
		   sizeof(SA));
    
    if (stat == -1) {
      error_exit("Couldn't connect socket");
    }
    
    return sock;
}



void main(int argc, char *argv[])
{
  char *serverName = NULL;
  int portNumber;
  int enableAnonSuites = 0;
  int enableCertSuites = 0;
  int enableNullSuites = 0;
  char tracebuf[512];
  char ciphers[512];
  char *clientMessage = "Message from OpenSSL client\n";
  char buff[512];
  char *serverResponse;
  int stat;
  int socket;
  int totalBytes;
  int messageComplete;
  int i;
  

  SSL_METHOD *meth = NULL;
  SSL_CTX *ctx;
  SSL *ssl;
  BIO *sbio;
  
  // parse command line args
  if (argc < 5) {
    usage();
    return;
  }
  
  serverName = argv[1];
  portNumber = atoi(argv[2]);
  enableAnonSuites = (strchr(argv[3],'a') != NULL);
  enableCertSuites = (strchr(argv[3],'c') != NULL);
  enableNullSuites = (strchr(argv[3],'n') != NULL);
  
  switch (atoi(argv[4]))
  {
  case 1 : meth = TLSv1_method(); break;
  case 2 : meth = TLSv1_1_method(); break;
  case 3 : meth = TLSv1_2_method(); break;
  case 4 : meth = SSLv3_method(); break;
  case 5 : meth = SSLv23_method(); break;
  default : usage(); return;
  }

  // trace argument is optional, but there's only one kind of
  // tracing we support
  if (argc > 5) {
    isTracing = 1;
  }

  checkEnvironment();
  
  trace(1,"Configuring SSL connection...");
  
  // Register all the libssl error strings
  trace(isTracing,"Calling SSL_load_error_strings()...");
  SSL_load_error_strings();
  
  // Register all available ciphers and digests.  The documentation
  // says you can disregard the return status
  trace(isTracing,"Calling SSL_library_init()...");
  SSL_library_init();
  
  // Seed the pseudo random number generator (PRNG).  If you don't do this
  // then certain SSL functions will complain "PRNG not seeded"
  trace(isTracing,"Calling RAND_seed()...");
  RAND_seed(randbuf,strlen(randbuf));
  
  // Now create the SSL context from which we will be able to create
  // SSL sessions
  trace(isTracing,"Calling SSL_CTX_new()...");
  ctx = SSL_CTX_new(meth);

  if (ctx == NULL) {
    error_exit("call to SSL_CTX_new() returned NULL");
  }

  // Tell SSL that we want verification of all server certificates.  Unless
  // this call is made, then any certificate presented by a server will
  // be acceptable.
  trace(isTracing,"SSL_CTX_set_verify()...");
  SSL_CTX_set_verify(ctx, 
		     SSL_VERIFY_PEER, 
		     NULL);

  // If the user has defined 'truststore', then tell SSL to use this file
  // as a source of CA certificates.
  if (getenv("truststore") != NULL) {
    trace(isTracing,"Calling SSL_CTX_load_verify_locations()...");
    stat = SSL_CTX_load_verify_locations(ctx,
					 getenv("truststore"), // CAfile
					 NULL                  // CApath
					 );
    if (stat != 1) {
      error_exit("SSL_CTX_load_verify_locations() failed");
    }
  }
   
  // Load the ciphers that they've asked for.
  // It could be inferred from the documentation for ciphers(1) that you 
  // can call SSL_CTX_set_cipher_list multiple times to build up a list 
  // of ciphers.  But that isn't how it works; rather you build up a list 
  // of ciphers in a string, and then pass that in a single call:

  // First of all disable any ciphers we've got by default
  ciphers[0] = 0;
  strcat(ciphers,"-ALL");
  
  // If they want cipher suites that require certificates, add them in
  if (enableCertSuites) {
    strcat(ciphers,":ALL:-aNULL");
  }

  // If they want the null ones, add them in
  if (enableNullSuites) {
    strcat(ciphers,":NULL");
  }

  // If they want the anonymous ones, add them in
  if (enableAnonSuites) {
    strcat(ciphers,":aNULL");
  }

  sprintf(tracebuf,"Calling SSL_CTX_set_cipher_list(\"%s\")...",ciphers);
  trace(isTracing,tracebuf);
  
  
  stat = SSL_CTX_set_cipher_list(ctx,ciphers);
  if (stat == 0) {
    error_exit("SSL_CTX_set_cipher_list() failed");
  }

  ssl = SSL_new(ctx);
  if (isTracing) {
    trace(1,"Enabled cipher suites are :");
    enumerate_ciphers(ssl);
  }
  
  trace(isTracing,"Connecting to server");
  socket = tcpConnect(serverName,portNumber);
  
  // Generate a BIO wrapper for the TCP socket
  trace(isTracing,"Calling BIO_new_socket()...");
  sbio = BIO_new_socket(socket,BIO_NOCLOSE);
  if (sbio == NULL) {
    error_exit("BIO_new_socket() failed");
  }
  
  // Connect up our SSL session with the socket
  trace(isTracing,"Calling SSL_set_bio()...");
  SSL_set_bio(ssl,sbio,sbio);
  
  // Initiate the SSL handshake
  trace(isTracing,"Calling SSL_connect()...");
  stat = SSL_connect(ssl);
  
  if (stat <= 0) {
    error_exit("SSL_connect failed");
  }
  
  // We are now ready to talk with the server
  trace(1,"\n...Now connected to server");
  
  trace(isTracing,"Calling SSL_write()...");
  stat = SSL_write(ssl,clientMessage,strlen(clientMessage));

  if (stat <= 0) {    
    error_exit("SSL_write failed");
  }
  
  if (stat != strlen(clientMessage)) {
    error_exit("Incomplete SSL write");
  }
  
  // Now read the response from the server, assuming it will be
  // terminated by EOL
  totalBytes = 0;
  messageComplete = 0;
  
  do {
    trace(isTracing,"Calling SSL_read()...");
    stat = SSL_read(ssl,
		    &buff[totalBytes],
		    (sizeof(buff) - totalBytes));
    if (stat <= 0) {
      error_exit("SSL_read failed");
    }
    
    // Look for an EOL
    for (i=0; i<stat; i++) {
      if (buff[(i+totalBytes)] == '\n') {
	
	// Stick a NULL terminator just beyond it
	buff[(i + totalBytes + 1)] = 0;
	messageComplete = 1;
	break;
      }
    }
    totalBytes += stat;
  }
  while (messageComplete == 0);
  
  sprintf(tracebuf,"The server sent -->%s<--", buff);
  trace(1,tracebuf);
  
  trace(isTracing,"Shutting down connection...");
  stat = SSL_shutdown(ssl);
  if (stat < 0) {
    error_exit("SSL_shutdown failed");
  }
  if (stat == 0) {
    // According to docs at the OpenSSL website, this means that the shutdown
    // has not yet finished, and we must call SSL_shutdown again..
    stat = SSL_shutdown(ssl);
    if (stat <= 0) {
      error_exit("SSL_shutdown (2nd time) failed");
    }
  }
  
  
  SSL_free(ssl);
  SSL_CTX_free(ctx);
  close(socket);
  
  trace(1,"\nProgam terminating");
  
}

